#!/usr/bin/env python
#
# Copyright 2013 Akamai Technologies, Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

import getopt
import hashlib
import base64
import hmac
import os
import sys
import logging
import urllib
import uuid
import re
from time import gmtime, strftime
from urlparse import urlparse

# data handling
# only handles -d/--data/--data-ascii/--data-binary
# no name
# only one entry, no repeats
# can handle @


def get_host(url, headers):
  host = ''
  if 'host' in headers:
    host = headers['host']
  else:
    parsed_url = urlparse(url)
    netloc = parsed_url.netloc
    if netloc.find(':') == -1:
      host = netloc
    else:
      host, port = netloc.split(':')
  return host.lower()


def get_relative_url(url):
    relative_url = ''
    auth_index = url.find('//')
    path_index = url.find('/', auth_index+2)
    if path_index == -1:
      relative_url = '/'
    else:
      relative_url = url[path_index:]
    return relative_url


def sign(data, key, algorithm):
  result = hmac.new(key, data, algorithm)
  return result.digest()
 

class EGSigner(object):
  def __init__(self, host, client_token, access_token, secret, max_body, signed_headers=None):
    self.host = host
    self.client_token = client_token
    self.access_token = access_token
    self.secret = secret
    self.max_body = max_body
    self.signed_headers = signed_headers


  def get_auth_header(self, url, method, headers, data_ascii, data_binary):  
    timestamp = strftime("%Y%m%dT%H:%M:%S+0000", gmtime())

    request_data = self.get_request_data(url, method, headers, data_ascii, data_binary)
    auth_data = self.get_auth_data(timestamp)
    request_data.append(auth_data)
    string_to_sign = '\t'.join(request_data)
    if verbose: print "String-to-sign: %s" %(string_to_sign)

    key_bytes = sign(bytes(timestamp), bytes(self.secret), hashlib.sha256)
    signing_key = base64.b64encode(key_bytes)
    signature_bytes = sign(bytes(string_to_sign), bytes(signing_key), hashlib.sha256)
    signature = base64.b64encode(signature_bytes)
    auth_header = 'Authorization: %ssignature=%s' %(auth_data, signature)
    return auth_header


  def get_auth_data(self, timestamp):
    auth_fields = []
    auth_fields.append('client_token=' + self.client_token)
    auth_fields.append('access_token=' + self.access_token)
    auth_fields.append('timestamp=' + timestamp)
    auth_fields.append('nonce=' + str(uuid.uuid4()))
    auth_fields.append('')
    auth_data = ';'.join(auth_fields)
    auth_data = 'EG1-HMAC-SHA256 ' + auth_data
    if verbose: print "Auth data: %s" %(auth_data)
    return auth_data


  def get_request_data(self, url, method, headers, data_ascii, data_binary):
    requst_data = []
    if not method:
      if data_ascii or data_binary:
        method = 'POST'
      else:
        method = 'GET'
    else:
      method = method.upper()
    requst_data.append(method)
    
    parsed_url = urlparse(url)
    requst_data.append(parsed_url.scheme)
    requst_data.append(self.host)
    requst_data.append(get_relative_url(url))
    requst_data.append(self.get_canonicalize_headers(headers))
    requst_data.append(self.get_content_hash(method, data_ascii, data_binary))
    return requst_data


  def get_canonicalize_headers(self, headers):
    canonical_header = '' 
    headers_values = []
    if verbose: print self.signed_headers
    for header_name in self.signed_headers:
      header_value = ''
      if header_name in headers:
        header_value = headers[header_name]
      if header_value:
        header_value = header_value.strip()
        p = re.compile('\\s+')
        new_value = p.sub(' ', header_value) 
        canonical_header = header_name + ':' + new_value
        headers_values.append(canonical_header)
    headers_values.append('')
    canonical_header = '\t'.join(headers_values)
    if verbose: print "Canonicalized header: %s" %(canonical_header)
    return canonical_header


  def get_content_hash(self, method, data_ascii, data_binary):
    content_hash = ''
    data = ''
    if data_ascii:
      data = data_ascii
    elif data_binary:
      data = data_binary

#    if method == 'POST' or method == 'PUT' or method == 'PATCH':
    # only hash POST for now
    if method == 'POST':
      if data:
        if data.startswith("@"):
          data_file = data.lstrip("@")
          try:
            if not os.path.isfile(data_file):
              raise Exception('%s is not a file' %(data_file))
            filesize = os.stat(data_file).st_size
            if filesize > self.max_body:
              raise Exception('File %s too large, max size is %s' %(data_file, self.max_body))
            # read the file content, and assign to data
            with open(data_file, "r") as f:
              data = f.read()
              if data_ascii:
                data = ''.join(data.splitlines())
          except IOError:
            raise
        else:
          if len(data) > self.max_body:
            raise Exception('Data too large, max size is %s' %(self.max_body))

        # compute the hash
        md = hashlib.sha256(data).digest()
        content_hash = base64.b64encode(md)
    return content_hash


config = os.environ["HOME"] + "/.egcurl"
section = "default"
verbose = False
method = None
data_ascii = None
data_binary = None
headers = {}
putback_args = []

usage = "Usage: %s [--eg-config CONFIG] [--eg-section SECTION] [--eg-verbose] CURL_ARGS+" % sys.argv[0]

args = sys.argv[1:]
try:
  opts, args = getopt.gnu_getopt(args,"H:X:d:F:GA:D:e:fiIkK:Lm:MNo:Oqr:RsSvw:y:Y:#",
    ["eg-config=","eg-section=","eg-verbose","header=","request=","data=","data-ascii=","data-binary=","data-urlencode=","form=","form-string=","get","user-agent=","compressed","connect-timeout=","create-dirs","dump-header=","referer=","fail","ignore-content-length","include","interface=","head","insecure","keepalive-time=","config=","limit-rate=","local-port=","location","max-filesize=","max-time=","manual","no-buffer","buffer","no-keepalive","keepalive","no-sessionid","sessionid","noproxy=","output=","remote-name","remote-name-all","no-remote-name","post301","post302","range=","raw","remote-time","retry=","retry-delay=","retry-max-time=","silent","show-error","stderr=","tcp-nodelay","trace=","trace-ascii=","trace-time","verbose","write-out=","speed-time=","speed-limit","progress-bar"])
except getopt.GetoptError:
  print >>sys.stderr, usage
  sys.exit(1)

for opt, arg in opts:
  if opt in ("-H", "--header"):
    putback_args.extend([opt, arg])
    if arg:
      header_field = arg.strip()
      header_name, header_value = header_field.split(':')
      if header_name:
        header_name = header_name.strip()
      if not header_name:
        print >>sys.stderr, "Invalid header value."
        sys.exit(1)
      if header_value:
        header_value = header_value.strip()
        if header_value:
          headers[header_name.lower()] = header_value
  elif opt in ("-X", "--request"):
    putback_args.extend([opt, arg])
    if method:
      print >>sys.stderr, "Multiple request methods are not supported."
      sys.exit(1)
    else:
      method = arg
  elif opt in ("-d", "--data", "--data-ascii"):
    putback_args.extend([opt, arg])
    if data_ascii or data_binary:
      print >>sys.stderr, "Multiple data arguments are not supported."
      sys.exit(1)
    else:
      data_ascii = arg
  elif opt in ("--data-binary"):
    putback_args.extend([opt, arg])
    if data_ascii or data_binary:
      print >>sys.stderr, "Multiple data arguments are not supported."
      sys.exit(1)
    else:
      data_binary = arg
  elif opt in ("-F", "--form", "--form-string"):
    print >>sys.stderr, "Form data is not supported."
    sys.exit(1)
  elif opt in ("--data-urlencode"):
    print >>sys.stderr, "Form data is not supported."
    sys.exit(1)
  elif opt in ("-G", "--get"):
    print >>sys.stderr, "The option %s is not supported." %opt
    sys.exit(1)
  elif opt == "--eg-config":
    config = arg
  elif opt == "--eg-section":
    section = arg
  elif opt == "--eg-verbose":
    verbose = True
  elif arg:
    putback_args.extend([opt, arg])
  else:
    putback_args.extend([opt])

if not len(args):
    print >>sys.stderr, "CURL_ARGS required"
    print >>sys.stderr, ""
    print >>sys.stderr, usage
    sys.exit(1)
    pass

url = args[-1]

host = get_host(url, headers)

with open(config, "r") as f:
  current_section = None
  vals = {}
  matched = False
  for line in f.readlines():
    if re.match("^\\s*($|#)", line): continue
    m = re.match("^\\s*\\[(.+?)\\]\\s*$", line)

    if m:
        current_section = m.group(1)
        continue

    vals = { "host": 0,
             "client_token": None,
             "access_token": None,
             "secret": None,
             "max-body": None,
             "signed-header": []}
    flds = line.split()
    for fld in flds:
        m = re.match("^([^:]+):(.+)$", fld)
        if not m: break
        name = m.group(1)
        val = m.group(2)
        if name not in vals: break
        if type(vals[name]) == list:
            vals[name].append(val)
        elif vals[name] == None or vals[name] == 0:
            vals[name] = val
        else:
            break
        if name == 'max-body':
            vals[name] = eval(val)
    else:
        if 0 not in vals.itervalues():
            if current_section == section:
                p = host.find(vals['host'])
                if p == 0: 
                    if verbose: print "Matched line for host %s" % host
                    matched = True
                    break
                pass
            continue
        pass
    raise ValueError("could not parse config line: " + line)
  if not matched:
    raise ValueError("could not find applicable config for host: " + host)

  if not vals['host']:
    raise ValueError("cannot find matching host")
  if not vals['client_token']:
    raise ValueError("client_token is not configured")
  if not vals['access_token']:
    raise ValueError("access_token is not configured")
  if not vals['secret']:
    raise ValueError("secret is not configured")
  if not vals['max-body']:
    raise ValueError("max-body is not configured")
 
  # update the args with the signature
  if verbose: print vals
  signer = EGSigner(host,
                    vals['client_token'],
                    vals['access_token'],
                    vals['secret'],
                    vals['max-body'],
                    vals['signed-header'])
  
  auth_header = signer.get_auth_header(url, method, headers, data_ascii, data_binary)

  args = (putback_args + args)
  args = (['-H', auth_header] + args)
  args = (['curl'] + args + ['-H', 'Expect:'])

  if verbose: print "args = %s" % (args,)
  sys.stdout.flush()

  os.execvp(args[0], args)



